<?php
/**
 ***** 扶摇而上、抱守本真 ******
 * @author Dana
 * @Date 2021/6/28
 */

function httpRangeRequest(){
    $file = 'numbers.txt';

    $content_type = 'text/plain';

    // 检查是否可读，并得到文件大小
    if (false === $fileLength = file($file)) {
        error_log("problem reading fileSize of $file");
    }

    // 解析首部来确定发生响应所需的信息
    if (isset($_SERVER['HTTP_RANGE'])) {
        // 定界符不区分大小写
        if (!preg_match('/bytes=\d*-\d*(,\d*-\d*)*$/i', $_SERVER['HTTP_RANGE'])) {
            error_log('Client requested invalid Range.');
            send_error($fileLength);
            exit;
        }

        $ranges = explode(',', substr($_SERVER['HTTP_RANGE'], 6));
        $offsets = [];

        // 抽取和验证每个部分
        // 只保存通过验证的部分
        foreach ($ranges as $range) {
            $offset = parse_offset($range, $fileLength);
            if ($offset !== false) {
                $offsets[] = $offset;
            }
        }

        /**
         * 取决于所请求的合法范围的个数
         */
        switch (count($offsets)) {
            case 0:
                // 非合法范围
                error_log("Clint requested no valid ranges.");
                send_error($fileLength);
                exit;
                break;
            case 1:
                // 一个合法范围，发生标准应答
                http_response_code(206);
                list($start, $end) = $offsets[0];
                header("Content-Range:bytes $start-$end/$fileLength");
                header("Content-Type:$content_type");

                // 设置变量，从而可以在这里以及下一个情况中重用代码
                $content_length = $end - $start + 1;
                $boundaries = [0 => '', 1 => ''];
                break;
            default:
                // 多个合法范围，发生多部分应答
                http_response_code(206);
                $boundary = str_rand(32);// 分隔各个部分的字符串

                /**
                 * 需要计算整个响应的内容长度
                 * 不过将整个响应加载到一个字符串中会占用大量内存，
                 * 所以使用偏移量计算值
                 * 另外利用这个机会计算边界
                 */
                $boundaries = [];
                $content_length = 0;

                foreach ($offsets as $offset) {
                    list($start, $end) = $offset;
                    // 用于分解各个部分
                    $boundary_header =
                        "\r\n" .
                        "--$boundary\r\n" .
                        "Content-Type:$content_type\r\n" .
                        "Content-Range: bytes $start-$end/$fileLength\r\n" .
                        "\r\n";
                    $content_length += strlen($boundary_header) + ($end - $start + 1);
                    $boundaries[] = $boundary_header;
                }

                // 增加结束边界
                $boundary_header = "\r\n--$boundary--";
                $content_length += strlen($boundary_header);
                $boundary_header[] = $boundary_header;

                // 去除第一个边界多余的\r\n
                $boundaries[0] = substr($boundaries[0], 2);
                $content_length -= 2;

                // 改为特殊的多部分内容类型
                $content_type = "multipart/byteranges;boundary=$boundary";
        }

    } else {
        // 发送整个文件
        // 设置变量，就好像这是从 Range 首部抽取的
        $start = 0;
        $end = $fileLength - 1;
        $offset = [$start, $end];
        $offsets = [$offset];

        $content_length = $fileLength;
        $boundaries = [0 => '', 1 => ''];
    }

    // 指出得到的是什么
    header("Content-Type:$content_type");
    header("Content-Length:$content_length");

    // 提供给用户
    $handle = fopen($file, 'r');
    if ($handle) {
        $offset_count = count($offsets);
        // 输出各个边界符和文件的适当部分
        for ($i = 0; $i < $offset_count; $i++) {
            print $boundaries[$i];
            list($start, $end) = $offsets[$i];
            send_range($handle, $start, $end);
        }
        // 结束边界
        print $boundaries[$i];
        fclose($handle);
    }
}

httpRangeRequest();

// 在文件中移动适当的位置
// 按块输出所请求的部分
function send_range($handle, $start, $end)
{
    $line_length = 4096;

    if(fseek($handle, $start) === -1){
        error_log("Error:fseek() fail.");
    }

    $left_to_read = $end - $start + 1;
    do{
        $length = min($line_length, $left_to_read);
        if(($buffer = fread($handle, $length)) !== false){
            print $buffer;
        }else{
            error_log("Error:fread() fail.");
        }
    }while($left_to_read -=$length);
}

// 发送失败首部
function send_error($fileLength){
    http_response_code(416);
    header("Content-Range:bytes */$fileLength");// 响应码 416 要求发送这个首部
}

// 将一个偏移量转换为文件中开始和结束位置
// 或者，如果偏移量是非法的，则返回false

function parse_offset($range, $fileLength){
    list($start,$end) = explode('-',$range);

    if($start === ''){
        if($end === '' || $end === 0){
            // 请求返回  "-" 或 "-0"
            return false;
        }else{
            $start = max(0,$fileLength - $end);
            $end = $fileLength - 1;
        }
    }else{

        if($end === '' || $end > $fileLength -1){
            $end = $fileLength - 1;
        }

        if($start > $end){
            return false;
        }
    }
    return [$start,$end];
}


function str_rand($length = 32,$characters = '0123456789abcdefghijklmnopqrstuvwyxzABCDEFGHIJKLMNOPQRSTUVWXYZ'){
    if(!is_int($length) || $length < 0){
        return false;
    }
    $characters_length = strlen($characters) -1;
    $string = '';

    for($i = $length;$i> 0;$i--){
        $string .=$characters[mt_rand(0,$characters_length)];
    }
    return $string;
}